Effective Java / Item 6

Item 6. 불필요한 객체 생성을 피하라.

Posted by Songi on 2019-05-18

재사용하자

1
String s = new String("hi");

위의 코드를 보면서 어떤생각이 들어야 할까. 책에서는 절대 따라하지 말라고 강력히 권고하고 있다. String 인스턴스는 문장이 실행될 때마다 새로 생성된다. 이 문장이 반복문 안에 있다면 생성되는 String 인스턴스의 갯수는 상상할 수 없다.

1
String s = "hi";

위의 코드는 어떨까. 앞의 코드와 정확히 같은 것을 나타낸다. 다만 다른점이 있다면 후자는 하나의 String 인스턴스를 사용한다는 점이다. 이 방식을 사용한다면 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.

똑같은 기능의 객체는 매번 생성하기 보다는 개체 하나를 재사용하는 편이 바람직하다. 재사용은 빠르고, 특히 불변 객체는 언제든 재사용될 수 있다.

정적 팩터리 메서드

재사용의 한 방법으로는 팩터리 메서드 사용이 있다. 팩터리 메서드를 사용하면 불필요한 객체 생성을 피할 수 있다. 예를들어 Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

1
2
3
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}

생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 그렇지 않다. 가변객체 또한 사용지 변경이 되지 않을것임을 확신한다면 재사용 가능하다.

캐싱

생상 비용이 비싼 객체는 캐싱하여 재사용하기를 권장하고 있다. 아래는 주어진 문자열이 우효한 로마 숫자인지 확인하는 메서드 이다.

1
2
3
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3}" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
코드 6-1 성능을 훨씬 더 끌어올릴 수 있다!

이 방식의 문제점은 String.matches 메서드를 사용하는데 있다. String.matches는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬은 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다. 이 메서드는 내부에서 정규표현식용 Pattern 인스턴스는 한번 쓰고 버려져 가비지 컬렉션 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태머신을 만들기 때문에 인스턴스 생성 비용이 높다.

유한 상태 머신이란 무엇인가


>유한 상태 기계는 다음과 같이 정의되어있습니다. 어떠한 사건(Event)에 의해 한 상태에서 다른 상태로 변화할 수 있으며, 이를 전이(Transition)이라 한다. (출처 : Wikipedia)

유한 상태 기계는 컴퓨터 프로그램과 전자 논리 회로를 설계하는데에 쓰이는 수학적 모델로써 자신이 취할 수 있는 유한한 갯수의 상태들을 가지며, 그 중에서 반드시 하나의 상태만 취한다. 또한, 현재 상태는 특정 조건이 되면 다른 상태로 변할 수 있다.

예를 들어 전구는 On/Off 두가지 상태만을 가지며, 둘중에 하나의 상태만을 취한다. 이같은 FSM은 다양한 이점이 있고 이는 State Design Pattern 과 관련이 있으므로 추후 따로 정리하도록 하겠다.


다시 캐싱으로 돌아와서, 앞의 코드의 성능을 개선하려면 정규표현식을 표현하는 불변의 Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생산에 캐싱해 두고, 나중에 isRomanNumeral 메서드 호출 될때마다 이 인스턴스를 재사용해야 한다.

1
2
3
4
5
6
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3}" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

public static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}

######
코드 6-2 값비싼 객체를 재사용하여 성능을 개선한다.

이후 성능이 향상되고 코드도 더 명확해 졌으며 의미 또한 잘 드러난다.

불필요한 객체를 만들어내는 예

오토박싱(auto boxing)

오토박싱이란 프로그래머가 기본 타입과 박싱된 기본타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다. 오토박싱은 기본타입과 그에 대응하는 박싱된 기본타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다. 의미상으로는 다를것이 없지만 성능상에서는 확실히 다르다.

1
2
3
4
5
6
7
private static long sum(){
Long sum= 0L;
for(long i=0; i<=Integer.MAX_VALUE;i++)
sum += i;

return sum;
}
코드 6-3 끔찍이 느리다! 객체가 만들어지는 위치를 찾았는가?

이는 모든 양의 정수의 총합을 구하는 메서드이다. long 타입인 i 가 Long 타입인 sum에 더해질때마다 문자 하나 때문에 불필요한 Long 인스턴스가 약 231개 만들어졌다. 단순히 sum의 타입을 long으로만 바꿔주면 6.3초에서 0.59초로 빨라진다.

박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자

끝마치며..

책에서는 객체 생성은 비싸다고 무조건 피해야 할 것이 아니며, 요즘의 JVM 으로는 작은 객체를 생성하고 회수하는 일은 큰 부담이 아니라고 말한다. 프로그램의 명확성, 간결성, 기능을 위해 객체를 추가하고 생성하는 것은 일반적으로 좋은일이라고 권고하기 까지 한다.

또한 데이터베이스 연결같이 생성 비용이 매우 비싸 재사용이 추천되는 경우가 아니라면 단순히 객체 생성을 피하고자 객체 풀 생성을 하지 말라고 말한다. 자체 객체 풀은 코드를 어지럽게 하며, 메모리 사용량을 늘리고 성능을 떨어뜨리는 주범이다.

마지막으로, 방어적 복사(Item50) 필요한 상황에서는 객체를 재사용하지 말라고 말한다. 방어적 복사란, 추후에 다루겠지만, 생성자로 받은 매개변수를 클래스내 매개변수에 직접 대입하지 않고, 새로운 객체를 만들어 대입하는 방식을 예로 들수 있다. 이런 방어적 복사로 인한 피해가 필요없는 객체를 반복생성했을 떄의 피해보다 훨씬크며, 버그와 보안구멍으로 이어지는 리스크가 또한 크다는게 저자의 의견이다.

출처

http://ozt88.tistory.com/8